/*
 * Copyright (c) 2012, marco.tamburelli@gmail.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer. 
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.bmi.gwt.mi.server.impl;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;

import javassist.CannotCompileException;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.CtPrimitiveType;
import javassist.NotFoundException;

import org.apache.log4j.Logger;
import org.bmi.gwt.mi.server.util.ServerUtil;
import org.bmi.gwt.mi.shared.MiObject;
import org.bmi.gwt.mi.shared.MiProxy;
import org.bmi.gwt.mi.shared.annotations.TargetMiObjectClass;
import org.bmi.gwt.mi.shared.communication.var.ReturnVar;
import org.bmi.gwt.mi.shared.exceptions.NotAvailableConstructorException;
import org.bmi.gwt.mi.shared.util.ProxyBase;
import org.bmi.gwt.mi.shared.util.ProxyFactory;

public class ProxyFactoryServer implements ProxyFactory
{
	static protected Logger logger = Logger.getLogger(ProxyFactoryServer.class);

	private HashMap<String, Class<? extends MiProxy>> classMap;
	private ClassPool classPool;

	public ProxyFactoryServer() throws NotFoundException
	{
		this.classMap = new HashMap<String, Class<? extends MiProxy>>();

		ClassPool parent = ClassPool.getDefault();
		this.classPool = new ClassPool(parent);
		this.classPool.insertClassPath(new ClassClassPath(getClass()));
	}

	@Override
	public ProxyBase createProxy(Class<? extends MiProxy> proxyType)
	{
		try
		{
			Class<? extends MiProxy> jClazz = getOrGenerateProxyClass(proxyType);
			return (ProxyBaseServer) jClazz.newInstance();
		}
		catch (Exception e)
		{
			/* This exception should never be thrown */
			logger.error("[ProxyFactoryServer] Impossible to create a proxy of type " + proxyType, e);
			return null;
		}
	}

	@Override
	public ProxyBase createProxy(Class<? extends MiProxy> proxyType, Object[] constructorArgs)
			throws NotAvailableConstructorException
	{
		try
		{
			Class<? extends MiProxy> jClazz = getOrGenerateProxyClass(proxyType);

			Constructor<? extends MiProxy> proxyConstructor = jClazz.getConstructor(Object[].class);
			Object args = constructorArgs;

			return (ProxyBaseServer) proxyConstructor.newInstance(args);
		}
		catch (Exception e)
		{
			logger.error("[ProxyFactoryServer] Impossible to create a proxy of type " + proxyType, e);
			throw new NotAvailableConstructorException("Impossible to create the proxy '" + proxyType.toString() + "'",
					e);
		}
	}

	@SuppressWarnings("unchecked")
	private Class<? extends MiProxy> getOrGenerateProxyClass(Class<? extends MiProxy> proxyType)
			throws NotFoundException, NotAvailableConstructorException, CannotCompileException, ClassNotFoundException,
			NoSuchMethodException, SecurityException, IOException
	{
		/* Adding the required class to the pool class path. */
		this.classPool.insertClassPath(new ClassClassPath(proxyType));

		String packageName = ProxyFactoryServer.class.getPackage().getName();
		String proxyClassName = packageName + "." + proxyType.getSimpleName() + "___Impl";

		Class<? extends MiProxy> jClazz = this.classMap.get(proxyClassName);

		if (jClazz == null)
		{
			TargetMiObjectClass b = proxyType.getAnnotation(TargetMiObjectClass.class);

			CtClass interf = this.classPool.get(proxyType.getName());
			CtClass base = this.classPool.get(ProxyBaseServer.class.getName());

			CtClass clazz = this.classPool.makeClass(proxyClassName, base);
			clazz.addInterface(interf);

			/*
			 * We don't make any validation here since the same validation is
			 * already made on client, and client will not compile if it's not
			 * valid.
			 */
			if (b == null)
				throw new NotAvailableConstructorException("The class " + proxyType.toString()
						+ " has no target class.");

			Class<? extends MiObject> targetClass = b.miObjectClass();

			/* Defining the default constructor */
			CtConstructor defConstr = new CtConstructor(new CtClass[] {}, clazz);
			defConstr.setBody("{super(" + targetClass.getName() + ".class);}");

			/* Defining the constructor */
			CtConstructor constr = new CtConstructor(new CtClass[] { this.classPool.get(Object[].class.getName()) },
					clazz);
			constr.setExceptionTypes(new CtClass[] { this.classPool.get(NotAvailableConstructorException.class
					.getName()) });

			StringBuilder ctorBody = new StringBuilder();
			ctorBody.append("{\n");
			ctorBody.append("	super(").append(targetClass.getName()).append(".class").append(", $$);\n");
			ctorBody.append("}");

			constr.setBody(ctorBody.toString());

			clazz.addConstructor(defConstr);
			clazz.addConstructor(constr);

			implementMethods(proxyType, interf, clazz);

			clazz.writeFile();

			// TODO ProtectionDomain could be required?

			jClazz = this.classPool.toClass(clazz);

			this.classMap.put(proxyClassName, jClazz);
		}

		return jClazz;
	}

	private Class<?> toJavaClass(CtClass ctClass) throws ClassNotFoundException, NegativeArraySizeException,
			NotFoundException
	{
		if (ctClass instanceof CtPrimitiveType) return ServerUtil.getType(((CtPrimitiveType) ctClass).getDescriptor());

		/* TODO I'm not sure it's a good method to get the type of the array */
		if (ctClass.isArray()) return Array.newInstance(toJavaClass(ctClass.getComponentType()), 0).getClass();

		return Class.forName(ctClass.getName());
	}

	private void implementMethods(Class<? extends MiProxy> proxyType, CtClass interf, CtClass generatedClass)
			throws NotFoundException, ClassNotFoundException, NoSuchMethodException, SecurityException,
			CannotCompileException
	{
		for (CtMethod interfaceMethod : interf.getDeclaredMethods())
		{
			String methodName = interfaceMethod.getName();

			StringBuilder argsSignature = new StringBuilder();

			/*
			 * Workaround to retrieve ReturnVar generic type, required to
			 * generate the method signature.
			 */
			CtClass[] ctParamTypes = interfaceMethod.getParameterTypes();
			Class<?>[] paramTypes = new Class<?>[ctParamTypes.length];

			for (int i = 0; i < ctParamTypes.length; i++)
				paramTypes[i] = toJavaClass(ctParamTypes[i]);

			Method _m = proxyType.getMethod(methodName, paramTypes);

			for (Type t : _m.getGenericParameterTypes())
				argsSignature.append(getTypeExpectedSignature(t));

			CtClass returnType = interfaceMethod.getReturnType();
			boolean returnVoid = returnType.equals(CtClass.voidType);

			StringBuilder body = new StringBuilder();
			body.append("{\n");

			if (returnVoid)
				body.append("	$proceed(");
			else
			{
				body.append("	return (");
				body.append(returnType.getName());
				body.append(") $proceed(");
			}

			body.append("super.objectId, ");
			body.append("super.targetMiObjectClass, ");
			body.append("\"").append(methodName).append("\", ");
			body.append("\"").append(argsSignature).append("\", ");

			body.append(ParamConverter.class.getCanonicalName()).append(".convertObjects(");
			body.append("$args)");

			if (returnVoid)
				body.append(");");
			else
			{
				body.append(", ");
				body.append("\"").append(getTypeExpectedSignature(_m.getGenericReturnType())).append("\", ");
				body.append(returnType.getName()).append(".class);");
			}

			body.append("}");

			CtMethod concreteProxyMethod = new CtMethod(returnType, methodName, ctParamTypes, generatedClass);
			concreteProxyMethod.setExceptionTypes(interfaceMethod.getExceptionTypes());
			concreteProxyMethod.setBody(body.toString(), "super.publisher", "publishMethod");

			generatedClass.addMethod(concreteProxyMethod);
		}
	}

	private static String getTypeExpectedSignature(Type t)
	{
		if (t instanceof ParameterizedType
				&& ReturnVar.class.isAssignableFrom((Class<?>) ((ParameterizedType) t).getRawType()))
		{
			ParameterizedType type = (ParameterizedType) t;
			return ServerUtil.getJNISignature((Class<?>) type.getActualTypeArguments()[0]);
		}
		else if (t instanceof Class && MiProxy.class.isAssignableFrom((Class<?>) t))
		{
			/*
			 * If the argument is a proxy, for the signature must be used the
			 * target object type.
			 */
			Class<?> p = (Class<?>) t;
			TargetMiObjectClass anno = p.getAnnotation(TargetMiObjectClass.class);

			if (anno != null)
				return (ServerUtil.getJNISignature(anno.miObjectClass()));
			else
				return ServerUtil.getJNISignature((Class<?>) t);
		}
		else
			return ServerUtil.getJNISignature((Class<?>) t);
	}
}
